package cn.leaf.calculator.old;

import cn.hutool.core.util.StrUtil;
import cn.leaf.calculator.CalException;
import org.springframework.util.Assert;

import java.io.Serializable;
import java.util.*;
import java.util.function.Function;

/**
 * @author karl lee
 * @date 2020/6/22
 */
public abstract class CalHelper<T> {

    /**
     * 参数池
     */
    private final Map<String, CalUnit<T>> argsPool;

    /**
     * 当前需要计算的单元Id集合
     */
    private final List<String> currentCalIds;

    /**
     * 计算等待区
     */
    private final List<CalUnit<T>> waitingArea;

    /**
     * 计算结果
     */
    private final List<T> result;

    /**
     * 更新参数池策略
     */
    private Function<Serializable, T> updateArgsPoolStrategy;

    public CalHelper(List<T> calList) {
        CalUnit<T> unit;
        argsPool = new HashMap<>();
        waitingArea = new ArrayList<>();
        currentCalIds = new ArrayList<>();
        result = new ArrayList<T>();
        for (T t : calList) {
            unit = buildCalUnit(t);
            // 所有单元为等待计算状态
            unit.setStatus(CalUnit.CAL_WAIT);
            // 添加到参数池
            argsPool.put(unit.getFormulaId(), unit);
            // 添加到等待区
            waitingArea.add(unit);
            // 记录当前需要计算的单元id
            currentCalIds.add(unit.getFormulaId());
        }
    }

    /**
     * 将一个需要计算的entity对象转换成一个计算单元对象
     *
     * @param entity entity
     * @return 计算单元
     */
    protected abstract CalUnit<T> buildCalUnit(T entity);

    /**
     * 计算逻辑，只需将计算好的结果放入entity即可
     *
     * @param currentCalUnit 需要计算的计算单元
     * @param referencedArgs 需要用到的参数
     */
    protected abstract void cal(CalUnit<T> currentCalUnit, List<CalUnit<T>> referencedArgs);

    /**
     * 计算成功之后，需要的操作，
     *
     * @param entity entity
     */
    protected abstract void calOkBefore(T entity);

    /**
     * 绑定参数池更新策略
     *
     * @param updateArgsPoolStrategy 参数池更新策略
     */
    public void bind(Function<Serializable, T> updateArgsPoolStrategy) {
        this.updateArgsPoolStrategy = updateArgsPoolStrategy;
    }

    /**
     * 开始执行
     *
     * @return 计算结果
     */
    public List<T> go() {
        Assert.notNull(updateArgsPoolStrategy, "参数池更新策略未绑定");
        // 等待区为空
        if (waitingArea.isEmpty()) {
            return getResult();
        }
        Iterator<CalUnit<T>> iterator = waitingArea.iterator();
        while (iterator.hasNext()) {
            CalUnit<T> unit = iterator.next();
            // 不存在公式，不计算
            if (!unit.hasFormula()) {
                ok(unit);
                iterator.remove();
                continue;
            }
            // 计算不允许自引用
            calAssess(unit.checkParamsIsSinceTheReference(), "公式中存在自引用，配置的参数不能是自身");

            // 计算不允许循环引用
            calAssess(isCircularReference(unit), "公式中存在循环引用，非法计算逻辑：A = B + C + D && B = A + C");
            if (!unit.isOk()) {
                cal(unit);
            }
            // 删除计算完成的数据
            if (unit.isOk()) {
                iterator.remove();
            }
        }
        return go();
    }

    /**
     * 计算
     *
     * @param unit 需要计算的计算单元
     */
    private void cal(CalUnit<T> unit) {
        // 获取参数
        List<CalUnit<T>> args = getArgs(unit.getFormulaParams());
        // 参数中有参数依赖当前项目成分的其他项，需要等待计算
        if (argsFromCurrentProject(args)) {
            return;
        }
        cal(unit, args);
        ok(unit);
    }

    /**
     * 计算成功
     *
     * @param unit 计算单元
     */
    private void ok(CalUnit<T> unit) {
        unit.ok();
        result.add(unit.getEntity());
    }

    /**
     * 更新参数池
     *
     * @param formulaId formulaId
     */
    private T updateArgsPool(Serializable formulaId) {
        T bean = updateArgsPoolStrategy.apply(formulaId);
        Assert.notNull(bean, "参数丢失");
        return bean;
    }

    /**
     * 获取参数
     *
     * @param formulaIds 成分ids
     * @return list
     */
    private List<CalUnit<T>> getArgs(List<Serializable> formulaIds) {
        List<CalUnit<T>> args = new ArrayList<>();
        for (Serializable comId : formulaIds) {
            args.add(getArgs(comId));
        }
        return args;
    }

    /**
     * 获取参数,如果该参数在参数池中没有，将使用参数池更新策略进行更新
     *
     * @param formulaId 成分id
     * @return CalUnit
     */
    private CalUnit<T> getArgs(Serializable formulaId) {
        if (argsPool.containsKey(formulaId)) {
            return argsPool.get(formulaId);
        }
        // 参数池中不存在。使用参数池更新策略
        CalUnit<T> newArgs = buildCalUnit(updateArgsPool(formulaId));
        // 新参数默认设置为计算完成状态
        newArgs.ok();
        argsPool.put(newArgs.getFormulaId(), newArgs);
        return getArgs(formulaId);
    }

    /**
     * 判断参数列表中的参数是否来自当前项目 并且该参数未计算完成
     *
     * @param args 参数列表
     * @return boolean
     */
    private boolean argsFromCurrentProject(List<CalUnit<T>> args) {
        for (CalUnit<T> arg : args) {
            // 参数是当前项目中的，并且参数未计算出结果
            if (isCurrentCals(arg.getFormulaId()) && !arg.isOk()) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否为循环引用 即：A = B + C + D && B = A + C
     *
     * @param unit 需要检查的计算单元
     * @return boolean
     */
    private boolean isCircularReference(CalUnit<T> unit) {
        List<Serializable> params = unit.getFormulaParams();
        for (Serializable param : params) {
            // 参数池中存在，并且未计算完成，并且计算该参数的值的公式涉及到的参数依赖本计算单元的值
            if (argsPool.containsKey(param) && !argsPool.get(param).isOk() && argsPool.get(param).paramsHas(unit.getFormulaId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否为当前需要计算的项中
     *
     * @param formulaId 公式id
     * @return bolean
     */
    private boolean isCurrentCals(String formulaId) {
        return currentCalIds.contains(formulaId);
    }

    /**
     * 是否允许计算，
     *
     * @param assess  是否允许
     * @param message 不允许抛出CalException 异常，message为异常信息
     */
    private void calAssess(boolean assess, String message) {
        if (assess) {
            throw new CalException(message);
        }
    }

    /**
     * 获取结果集， 需执行go方法后获取，否则获取一个空集合
     *
     * @return list
     */
    public List<T> getResult() {
        return result;
    }

    /**
     * 计算单元
     */
    protected class CalUnit<E> {
        /**
         * 计算状态：成功
         */
        protected static final String CAL_SUCCESS = "success";
        /**
         * 计算状态：等待
         */
        protected static final String CAL_WAIT = "wait";
        /**
         * 公式id
         */
        private String formulaId;
        /**
         * 公式
         */
        private String formulaString;
        /**
         * 计算bean
         */
        private E entity;
        /**
         * 计算状态
         */
        private String status;

        private CalFormula formulaScript;

        public CalUnit(String comId, E entity) {
            this.formulaId = comId;
            this.entity = entity;
            status = CAL_WAIT;
        }

        public CalUnit(String comId, E entity, String formula) {
            this.formulaId = comId;
            this.entity = entity;
            this.formulaString = formula;
            status = CAL_WAIT;
        }

        public CalUnit(String comId, E entity, String formula, String status) {
            this.formulaId = comId;
            this.entity = entity;
            this.formulaString = formula;
            this.status = status;
        }

        public boolean isOk() {
            return CAL_SUCCESS.equals(status);
        }

        public boolean hasFormula() {
            return !StrUtil.isBlank(formulaString);
        }

        protected CalFormula buildScriptCalFormula() {
            formulaScript = new CalFormula(formulaString);
            return formulaScript;
        }

        protected List<Serializable> getFormulaParams() {
            if (formulaScript == null) {
                buildScriptCalFormula();
            }
            return formulaScript.getParams();
        }

        private String getStatus() {
            return status;
        }

        private void setStatus(String status) {
            this.status = status;
        }

        public boolean checkParamsIsSinceTheReference() {
            for (Serializable param : getFormulaParams()) {
                if (formulaId.equals(param)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 参数中是否存在某个id
         *
         * @param checkId 需要检测的id
         * @return
         */
        public boolean paramsHas(String checkId) {
            for (Serializable param : getFormulaParams()) {
                if (checkId.equals(param)) {
                    return true;
                }
            }
            return false;
        }

        public CalFormula getFormulaScript() {
            if (formulaScript == null) {
                buildScriptCalFormula();
            }
            return formulaScript;
        }

        public String getFormulaId() {
            return formulaId;
        }

        public void setFormulaId(String formulaId) {
            this.formulaId = formulaId;
        }

        public E getEntity() {
            return entity;
        }

        public void setEntity(E entity) {
            this.entity = entity;
        }


        public String getFormulaString() {
            return formulaString;
        }

        public void setFormulaString(String formulaString) {
            this.formulaString = formulaString;
        }

        public void ok() {
            status = CAL_SUCCESS;
        }
    }
}
